package io.adrainty.bolee.security.satoken.handler;

import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.data.model.AccessTokenModel;
import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;
import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;
import cn.dev33.satoken.oauth2.granttype.handler.SaOAuth2GrantTypeHandlerInterface;
import cn.dev33.satoken.stp.StpLogic;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.StrPool;
import io.adrainty.bolee.framework.commons.basic.ResponseCode;
import io.adrainty.bolee.framework.commons.basic.ResultDTO;
import io.adrainty.bolee.framework.commons.constants.GlobalOauth2Constants;
import io.adrainty.bolee.framework.commons.exception.AppException;
import io.adrainty.bolee.framework.commons.user.DataSecurityEntity;
import io.adrainty.bolee.framework.commons.user.UserEntity;
import io.adrainty.bolee.framework.commons.utils.EnumUtil;
import io.adrainty.bolee.framework.commons.utils.ResultDTOUtil;
import io.adrainty.bolee.framework.commons.utils.StpKit;
import io.adrainty.bolee.security.feign.ICustomerFeign;
import io.adrainty.bolee.security.feign.IUserFeign;
import io.adrainty.bolee.security.models.constants.CompanyConstants;
import io.adrainty.bolee.security.models.constants.OAuth2Constants;
import io.adrainty.bolee.security.models.entity.CompanyEntity;
import io.adrainty.bolee.security.models.entity.ResourceEntity;
import io.adrainty.bolee.security.models.entity.RoleEntity;
import io.adrainty.bolee.security.models.request.QueryDataSecurityDTO;
import io.adrainty.bolee.security.models.valobj.UserTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * <p>BaseGrantTypeHandler</p>
 *
 * @author AdRainty
 * @version V1.0.0
 * @description BaseGrantTypeHandler
 * @since 2025/8/6 13:55:17
 */

@Slf4j
public abstract class BaseGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface {

    @Resource
    protected IUserFeign userFeign;

    @Resource
    protected ICustomerFeign customerFeign;

    @Resource
    protected RedissonClient redissonClient;

    @Override
    public AccessTokenModel getAccessToken(SaRequest req, String clientId, List<String> scopes) {
        CompanyEntity companyEntity = beforeHandler(req);
        Map<String, String> paramMap = req.getParamMap();
        UserTypeEnum userType = getUserType(paramMap);
        UserEntity userEntity = doGrantHandler(userType, paramMap, companyEntity);
        return afterHandler(userType, userEntity, clientId, scopes);
    }

    /**
     * 域名校验
     *
     * @param request 请求参数
     * @return CompanyEntity
     */
    protected final CompanyEntity beforeHandler(SaRequest request) {
        String remoteHost = getRemoteHost(request);
        String key = CompanyConstants.COMPANY_WEBSITE_CACHE_KEY + remoteHost;

        RBucket<CompanyEntity> bucket = redissonClient.getBucket(key);
        CompanyEntity companyEntity = bucket.get();

        if (Objects.isNull(companyEntity)) {
            log.error("域名校验失败, remoteHost: {}", remoteHost);
            throw new SaOAuth2Exception(ResponseCode.LOGIN_DOMAIN_ERROR.getDesc());
        }

        return companyEntity;
    }

    /**
     * 获取用户信息
     *
     * @param userType   用户类型
     * @param userDetail 用户信息
     * @param clientId   客户端ID
     * @param scopes     授权范围
     * @return AccessTokenModel
     */
    protected final AccessTokenModel afterHandler(UserTypeEnum userType, UserEntity userDetail, String clientId, List<String> scopes) {
        AccessTokenModel accessTokenModel = buildAccessTokenModel(userDetail.getId(), clientId, scopes);
        long expiresIn = accessTokenModel.getExpiresIn();
        long refreshExpiresIn = accessTokenModel.getRefreshExpiresIn();
        String accessToken = accessTokenModel.getAccessToken();
        String refreshToken = accessTokenModel.getRefreshToken();

        StpLogic stpLogic = StpKit.getStpLogic(userType.getCode());
        stpLogic.login(userDetail.getId());
        String sessionTokenId = stpLogic.getTokenValue();
        userDetail.setUserToken(sessionTokenId);

        if (!Boolean.TRUE.equals(userDetail.getOnlyAuthenticate())) {
            // Get Authority Resource
            ResultDTO<List<ResourceEntity>> resourceResult = userFeign.findResourceByUserId(userDetail.getId());
            List<ResourceEntity> resourceEntities = ResultDTOUtil.assertResultDataNotNull(resourceResult);
            userDetail.setResourceRequestPaths(
                    resourceEntities.stream().map(ResourceEntity::getRequestPath).collect(Collectors.toSet())
            );

            ResultDTO<List<RoleEntity>> roleResult = userFeign.findRoleByUserId(userDetail.getId());
            List<RoleEntity> roleEntities = ResultDTOUtil.assertResultDataNotNull(roleResult);
            userDetail.setRoleLabels(roleEntities.stream().map(RoleEntity::getRoleName).collect(Collectors.toSet()));

            QueryDataSecurityDTO dataSecurityDTO = new QueryDataSecurityDTO(roleEntities, userDetail.getId());
            ResultDTO<DataSecurityEntity> securityResult = userFeign.userDataSecurity(dataSecurityDTO);
            DataSecurityEntity dataSecurityEntity = ResultDTOUtil.assertResultDataNotNull(securityResult);
            userDetail.setDataSecurityVO(dataSecurityEntity);
        }

        // //设置userVO到redis，获得时候使用userToken获得
        RBucket<UserEntity> tokenBucket = redissonClient.getBucket(GlobalOauth2Constants.USER_TOKEN_CACHE + sessionTokenId);
        setBucketVal(tokenBucket, userDetail, expiresIn);

        // 设置accessToken到redis，获得时使用userToken获取
        RBucket<String> accessTokenBucket = redissonClient.getBucket(GlobalOauth2Constants.ACCESS_TOKEN_CACHE + accessToken);
        setBucketVal(accessTokenBucket, accessToken, expiresIn);

        // 设置refreshToken到redis，获得时使用userToken获取
        RBucket<String> refreshTokenBucket = redissonClient.getBucket(GlobalOauth2Constants.REFRESH_TOKEN_CACHE + sessionTokenId);
        setBucketVal(refreshTokenBucket, refreshToken, refreshExpiresIn);

        // 脱敏处理下
        userDetail.desensitize();
        Map<String, Object> beanToMap = BeanUtil.beanToMap(userDetail);
        accessTokenModel.setExtraData(beanToMap);
        return accessTokenModel;
    }

    /**
     * 实际授权方法
     *
     * @param userType      用户类型
     * @param parameters    请求参数
     * @param companyEntity 公司信息
     * @return 授权类型
     */
    protected abstract UserEntity doGrantHandler(UserTypeEnum userType, Map<String, String> parameters, CompanyEntity companyEntity);

    /**
     * 获取授权类型
     *
     * @return 授权类型
     */
    public abstract String getActualGrantType();

    /**
     * 构建 AccessTokenModel
     *
     * @param clientId 客户端ID
     * @param scopes   授权范围
     * @param loginId  登录ID
     * @return AccessTokenModel
     */
    protected final AccessTokenModel buildAccessTokenModel(Long loginId, String clientId, List<String> scopes) {
        RequestAuthModel ra = new RequestAuthModel();
        ra.clientId = clientId;
        ra.loginId = loginId;
        ra.scopes = scopes;

        // 4、生成 Access-Token
        return SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true,
                atm -> atm.grantType = getHandlerGrantType());
    }

    /**
     * 获取用户类型
     *
     * @param parameter 请求参数
     * @return UserTypeEnum
     */
    protected final UserTypeEnum getUserType(Map<String, String> parameter) {
        String loginType = parameter.get(OAuth2Constants.LOGIN_TYPE_KEY);
        try {
            loginType = loginType.split(StrPool.DASHED)[0];
            return EnumUtil.getEnumConstants(loginType, UserTypeEnum.class);
        } catch (Exception e) {
            log.error("LoginType parameter error, accept loginType: {}", loginType);
            throw new AppException(ResponseCode.ILLEGAL_PARAMETER);
        }
    }

    protected final <T> void setBucketVal(RBucket<T> bucket, T value, long expireIn) {
        if (expireIn < 0) {
            bucket.set(value);
        } else {
            bucket.set(value, Duration.ofSeconds(expireIn));
        }
    }

    private String getRemoteHost(SaRequest request) {
        String host = request.getHeader("x-forwarded-host");
        if (StringUtils.isBlank(host)) {
            host = request.getHeader("host");
        }
        if (StringUtils.isBlank(host)) {
            host = request.getHost();
        }
        return host;
    }

}
